package none.rg.gantt.model;


import java.util.*;


/**
 * Class which provides mechanisms for calculation of chart (calculation
 * of tasks start and finish times depending on their duration and links
 * between them.
 * @author Rodion Gorkovenko
 */
public class Calculator {
    /*
     * TODO!!! refactor this class and its inners into separate package
     * Remove unnecessary "public"s
     * See Issue9
     */
    
    
    private Chart chart;
    private int endTime;
    private Map<Integer, Node> nodes = new HashMap<>();
    
    
    /**
     * Public constructor. It calls creation of backing structure for the
     * chart on which real calculations are later performed with {@link #process process()} method.
     * @param chart link to chart on which to perform calculations
     */
    public Calculator(Chart chart) {
        this.chart = chart;
        createStructure();
    } // Calculator
    
    
    /**
     * Protected constructor for tests. It does not create backing structure.
     */
    protected Calculator() {}
    
    
    /**
     * Separate initializer for tests. It calls backing structure creation.
     * @param chart link to chart on which to perform calculations
     */
    protected void init(Chart chart) {
        this.chart = chart;
        createStructure();
    } // init
        

    /**
     * Creates backing structure on which calculations are performed.
     * Each tasks is represented with two nodes (for start and finish).
     * Links between such nodes are represented by Edges (which allows avoid
     * regarding of links types).
     */
    private void createStructure() {
        
        for (Task task : chart.getTaskMap().values()) {
            new Edge(new Node(task, true), new Node(task, false), task.getDuration());
        } // for
        
        for (Task task : chart.getTaskMap().values()) {
            for (Link.Id linkId : task.getSuccIds()) {
                addEdge(task.getSucc(linkId));
            } // for
        } // for
        
    } // createStructure
    
    
    /**
     * Performs calculation process. Backing structure should be prepared
     * via call to public constructor or {@link #init init(Chart)} method.
     * @return true if calculation is successful.
     */
    public boolean process() {
        int total = 0, processed;
        Set<Node> nodeSet = new HashSet<>(nodes.values()); 
        
        endTime = 0;
        
        for (Node node : nodeSet) {
            node.setStart(0);
            node.setFinish(0);
        } // for
        
        do {
            processed = 0;
            for (Iterator<Node> it = nodeSet.iterator(); it.hasNext(); ) {
                Node node = it.next();
                if (node.getIns() == 0) {
                    processed++;
                    total++;
                    it.remove();
                    
                    for (Edge edge : node.getOutgoing()) {
                        int s, t;
                        Node to = edge.getTo();
                        to.setIns(to.getIns() - 1);
                        t = node.getStart() + edge.getLen();
                        if ((s = to.getStart()) < t) {
                            to.setStart(t);
                        } else {
                            t = s;
                        } // else
                        if (endTime < t) {
                            endTime = t;
                        } // if
                    } // for
                } // if
            } // for
        } while (processed > 0);
        
        if (total < nodes.size()) {
            return false;
        } // if
        
        nodeSet = new HashSet<>(nodes.values());
        
        do {
            processed = 0;
            for (Iterator<Node> it = nodeSet.iterator(); it.hasNext(); ) {
                Node node = it.next();
                if (node.getOuts() == 0) {
                    processed++;
                    it.remove();
                    
                    for (Edge edge : node.getIncoming()) {
                        int t;
                        Node from = edge.getFrom();
                        from.setOuts(from.getOuts() - 1);
                        t = node.getFinish() + edge.getLen();
                        if (from.getFinish() < t) {
                            from.setFinish(t);
                        } // if
                    } // for
                } // if
            } // for
        } while (processed > 0);
        
        for (Node node : nodes.values()) {
            node.setFinish(endTime - node.getFinish());
        } // for
        
        storeResults();
        
        return true;
    } // process
    
    
    /**
     * Allows to retrieve largest finish time after calculations.
     */
    public int getEndTime() { return endTime; }
    
    
    private void storeResults() {
        Map<Integer, Task> tasks = chart.getTaskMap();
        
        for (Node node : nodes.values()) {
            int id = node.getId();
            
            if (id % 2 == 0) {
                tasks.get(id / 2).setStart(node.getStart());
            } else {
                tasks.get(id / 2).setFinish(node.getStart());
            } // else
            
        } // for
        
        for (Map.Entry<Integer, Task> ent : tasks.entrySet()) {
            Node start = nodes.get(ent.getKey() * 2);
            Node finish = nodes.get(ent.getKey() * 2 + 1);
            Task task = ent.getValue();
            
            if (task.isFixedDuration()) {
                task.setCritical(start.getStart() == start.getFinish()
                        ? Task.Critical.FULL : Task.Critical.NONE);
                continue;
            } // if
            
            // may be written more compact but less clear with single 4-values flag
            if (finish.getStart() == finish.getFinish()) {
                task.setCritical(start.getStart() == start.getFinish()
                        ? Task.Critical.FULL : Task.Critical.FINISH);
            } else {
                task.setCritical(start.getStart() == start.getFinish()
                        ? Task.Critical.START : Task.Critical.NONE);
            } // else
        } // for
        
    } // storeResults
    
    
    /**
     * Allows to retrieve nodes map for testing
     */
    protected Map<Integer, Node> getNodes() {
        return nodes;
    } // getNodes
    
    
    private void addEdge(Link link) {
        Task pred = link.getPred();
        Task succ = link.getSucc();
        int predId = pred.getId() * 2, succId = succ.getId() * 2;
        
        int len = 0;

        switch (link.getType()) {
        case FINISH_FINISH:
            predId++;
            succId++;
            break;
        case FINISH_START:
            predId++;
            break;
        case START_FINISH:
            succId++;
            break;
        } // switch
        
        if (pred.isFixedDuration() && predId % 2 != 0) {
            predId--;
            len += pred.getDuration();
        } // if
        if (succ.isFixedDuration() && succId % 2 != 0) {
            succId--;
            len -= succ.getDuration();
        } // if
        
        new Edge(nodes.get(predId), nodes.get(succId), len);
    } // addEdge
    
    
    /**
     * Class representing node of backing structure - the beginning
     * or ending of a task. It is visible for test purposes.
     */
    protected class Node {
        
        private int id;
        private int start, finish;
        private List<Edge> incoming = new LinkedList<>();
        private List<Edge> outgoing = new LinkedList<>();
        private int ins = 0, outs = 0;
        
        public Node(Task task, boolean start) {
            id = task.getId() * 2 + (start ? 0 : 1);
            nodes.put(id, this);
        } // Node
        
        public int getId() { return id; }
        
        public int getIns() { return ins; }
        public void setIns(int v) { ins = v; }
        
        public int getOuts() { return outs; }
        public void setOuts(int v) { outs = v; }
        
        public int getStart() { return start; }
        public void setStart(int v) { start = v; }
        
        public int getFinish() { return finish; }
        public void setFinish(int v) { finish = v; }
        
        public List<Edge> getOutgoing() {
            return outgoing;
        } // getOutgoing
        
        public List<Edge> getIncoming() {
            return incoming;
        } // getIncoming
        
        @Override
        public boolean equals(Object o) {
            return (o instanceof Node) && ((Node) o).getId() == id;
        } // equals
        
        @Override
        public int hashCode() { return id; }
        
    } // class Node
    
    
    private static class NodeByInsCmp implements Comparator<Node> {
        
        public int compare(Node a, Node b) {
            return Integer.compare(a.getIns(), b.getIns());
        } // compare
        
    } // class NodeByInsCmp
    
    
    /**
     * Class for representing links between task starts or ends (nodes).
     * It is made visible for test purposes.
     */
    protected static class Edge {
        
        private int len;
        private Node from, to;
        
        public Edge(Node from, Node to, int len) {
            this.from = from;
            this.to = to;
            this.len = len;
            from.outgoing.add(this);
            to.incoming.add(this);
            from.setOuts(from.getOuts() + 1);
            to.setIns(to.getIns() + 1);
        } // Edge
        
        public Node getTo() { return to; }
        
        public Node getFrom() { return from; }
        
        public int getLen() { return len; }
        
    } // class Edge
    
    
} // Calculator
